spectator = {};
spectator.server = null;

spectator.onload = function() {
  spectator.debug("spectator started");
  spectator.debug("Version 20080604");
  spectator.start();
};

spectator.onunload = function() {
  spectator.stop();
};

spectator.debug = function(val) {
  if (val.constructor != String) {
    val = JSON.stringify(val, null, 2);
  }
  var log = document.getElementById("log");
  log.value = log.value ? (log.value + "\n" + val) : val;
  //console.log(val);
};

spectator.start = function() {
  if (!spectator.server) {
    var port = 9000;
    spectator.server = new suck.Server(spectator.handleConnection, port, true);
    spectator.server.start();
    spectator.debug("Listening on port " + port);
  }
};

spectator.stop = function() {
  if (spectator.server) {
    spectator.server.stop();
    spectator.server = null;
    spectator.debug("Stopped");
  }
};

spectator.restart = function() {
  spectator.stop();
  spectator.start();
};

spectator.handleConnection = function(connection) {
  spectator.debug("Incoming connection");
  // todo: let application take focus, but window.focus() only works the first time ?!?
  var handler = {
    oninit: function(packet) {
      controller.oninit(packet);
    },
    onerror: function(error) {
      controller.onerror(error);
    }
  };
  var session = new dbgp.Session(connection, bind("oninit", handler), bind("onerror", handler));
  var remoteFiles = new spectator.RemoteFileAccess(session);
  var breakpoints = new spectator.BreakPoints(session);
  var view = new spectator.SessionWindowView(
    document.getElementById("tabs"),
    document.getElementById("tabpanels"),
    remoteFiles,
    breakpoints);
  var controller = new spectator.SessionWindowController(session, view);
  connection.onclose = bind("onclose", controller);
};

spectator.RemoteFileAccess = function(session) {
  this.session = session;
  this.cache = {};
};

spectator.RemoteFileAccess.prototype.getFile = function(filename) {
  var d = new Deferred();
  var self = this;
  if (typeof(this.cache[filename]) == "undefined") {
    if (filename.match(/\([0-5]+\) : .*$/)) {
      self.cache[filename] = -1;
      d.errback(null);
    } else {
      this.session.queryInterface.source(filename)
        .addCallback(
          function(data) {
            self.cache[filename] = data;
            d.callback(data);
          })
        .addErrback(
          function(error) {
            if (error.code == 100) {
              self.cache[filename] = -1;
              d.errback(null);
            } else {
              // todo: better error-handling
              spectator.debug(["RemoteFileAccess error", error]);
            }
          });
    }
  } else if (this.cache[filename] == -1) {
    d.errback(null);
  } else {
    d.callback(self.cache[filename]);
  }
  return d;
};

spectator.BreakPoints = function(session) {
  this.session = session;
  this.points = [];
  this.eventListeners = {
    onupdate: []
  };
};

spectator.BreakPoints.prototype.addOnUpdate = function(callback) {
  this.eventListeners.onupdate.push(callback);
};

spectator.BreakPoints.prototype.hasBreakpoint = function(file, lineno) {
  try {
    mapcar(
      function(point) {
        if (point.file == file && point.lineno == lineno) {
          throw {yield: true};
        }
      },
      this.points);
  } catch (err) {
    if (err.yield) {
      return true;
    }
    throw err;
  }
  return false;
};

spectator.BreakPoints.prototype.addBreakpoint = function(file, lineno) {
  if (!this.hasBreakpoint(file, lineno)) {
    var self = this;
    this.session.queryInterface.breakpointSetLine(file, lineno)
    .addCallback(
      function(response) {
        self.points.push({file: file, lineno: lineno, id: response.id});
        mapcar(
          function(callback) {
            callback();
          },
          self.eventListeners.onupdate);
      }
    )
    .addErrback(bind("debug", spectator));
  }
};

spectator.BreakPoints.prototype.removeBreakpoint = function(file, lineno) {
  var self = this;
  mapcar(
    function(point) {
      if (point.file == file || point.lineno == lineno) {
        self.removeBreakpointById(point.id);
      }
    },
    this.points);
};

spectator.BreakPoints.prototype.removeBreakpointById = function(id) {
  var tmp = [];
  mapcar(
    function(point) {
      if (point.id != id) {
        tmp.push(point);
      }
    },
    this.points);
  this.points = tmp;
  mapcar(
    function(callback) {
      callback();
    },
    this.eventListeners.onupdate);
};

spectator.BreakPoints.prototype.getAll = function() {
  return this.points;
};

spectator.BreakPoints.prototype.getForFile = function(file) {
  var tmp = [];
  mapcar(
    function(point) {
      if (point.file == file) {
        tmp.push(point);
      }
    },
    this.points);
  return tmp;
};

spectator.BreakPoints.prototype.getById = function(id) {
  return this.points[id];
};

spectator.SessionWindowController = function(session, view) {
  this.session = session;
  this.view = view;
  this.view.create(this);
};

spectator.SessionWindowController.prototype.oninit = function(packet) {
  if (packet.protocol_version != "1.0") {
    this.onerror("Protocol version not supported");
  }
  this.filename = unescape(packet.fileuri);
  this.lineno = 1;
  this.status = packet.status;
  this.view.setSessionName(this.filename.match(/[^\/]+$/)[0]);
  this.view.onfilenamechange(this.filename, this.lineno);
  this.session.queryInterface.setFeature("max_depth", 8)
  .addCallback(bind("onstepinto", this))
  .addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onerror = function(err, session) {
  spectator.debug(err);
  this.session.close();
  spectator.restart();
};

spectator.SessionWindowController.prototype.onclose = function() {
  this.session.close();
  spectator.restart();
  this.view.destroy();
};

spectator.SessionWindowController.prototype.onstatusstopping = function() {};

spectator.SessionWindowController.prototype.updateProgramCounter = function(filename, lineno, adviseStackListeners) {
  if (typeof(adviseStackListeners) == "undefined") {
    adviseStackListeners = true;
  }
  var oldfilename = this.filename;
  var oldlineno = this.lineno;
  this.filename = filename;
  this.lineno = lineno;
  if (oldfilename != this.filename) {
    this.view.onfilenamechange(this.filename, this.lineno);
  } else if (oldlineno != this.lineno) {
    this.view.onlinenochange(this.lineno);
  }
  if (adviseStackListeners && this.status != "stopped" && (oldfilename != this.filename || oldlineno != this.lineno)) { // hvad med || status ???
    this.session.queryInterface.getCallstack().addCallback(bind("onstackupdate", this.view)).addErrback(bind("onerror", this));
  }
};

spectator.SessionWindowController.prototype.onupdateprogramcounter = function(response) {
  if (response.status != this.status) {
    this.status = response.status;
    this["onstatus" + this.status]();
  }
  if (typeof(response["xdebug:message"]) != "undefined") {
    this.updateProgramCounter(unescape(response["xdebug:message"].filename), response["xdebug:message"].lineno);
  }
};

spectator.SessionWindowController.prototype.oncontextupdate = function(contexts) {
  this.view.oncontextupdate(contexts);
};

spectator.SessionWindowController.prototype.onselectstack = function() {
  var depth = this.view.callstack.selectedItem.value;
  this.session.queryInterface.getContext(depth).addCallback(bind("oncontextupdate", this)).addErrback(bind("onerror", this));
  // todo: update view.file
  var self = this;
  this.session.queryInterface.stackGet(depth)
  .addCallback(
    function(stack) {
      self.updateProgramCounter(unescape(stack.filename), stack.lineno, false);
    })
  .addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onrun = function() {
  this.session.queryInterface.run().addCallback(bind("onupdateprogramcounter", this)).addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onstop = function() {
  this.session.queryInterface.stop().addCallback(bind("onupdateprogramcounter", this)).addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onstepinto = function() {
  this.session.queryInterface.stepInto().addCallback(bind("onupdateprogramcounter", this)).addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onstepover = function() {
  this.session.queryInterface.stepOver().addCallback(bind("onupdateprogramcounter", this)).addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onstepout = function() {
  this.session.queryInterface.stepOut().addCallback(bind("onupdateprogramcounter", this)).addErrback(bind("onerror", this));
};

spectator.SessionWindowController.prototype.onstatusbreak = function() {
};

spectator.SessionWindowController.prototype.onstatusstopped = function() {
  // todo: maybe allow user to do stuff here, but for now we just end the session
  this.session.close();
};


spectator.SessionWindowView = function(tabs, tabpanels, remoteFiles, breakpoints) {
  this.tabs = tabs;
  this.tabpanels = tabpanels;
  this.remoteFiles = remoteFiles;
  this.breakpoints = breakpoints;
  this.fileAvailable = false;
  this.breakpoints.addOnUpdate(bind("updateBreakpoint", this));
};

spectator.SessionWindowView.prototype.setSessionName = function(name) {
  var self = this;
  var count = 0;
  mapcar(
    function(tab) {
      if (tab != self.tab && tab.getAttribute("label").replace(/ \[[0-9]+\]$/, "") == name) {
        count++;
      }
    },
    this.tabs.parentNode.getElementsByTagName("tab"));
  if (count > 0) {
    this.tab.setAttribute("label", name + " [" + (count + 1) + "]");
  } else {
    this.tab.setAttribute("label", name);
  }
};

spectator.SessionWindowView.prototype.create = function(controller) {
  try {

  var document = this.tabs.ownerDocument;
  var tabpanel_id = (new Date()).getTime();

  this.tab = document.createElement("tab");
  this.tab.setAttribute("linkedpanel", tabpanel_id);
  this.tabs.appendChild(this.tab);

  this.tabpanel = document.createElement("tabpanel");
  this.tabpanel.setAttribute("id", tabpanel_id);
  this.tabpanels.appendChild(this.tabpanel);

  var vbox = document.createElement("vbox");
  this.tabpanel.appendChild(vbox);

  var splitter = document.createElement("splitter");
  splitter.setAttribute("collapse", "before");
  splitter.appendChild(document.createElement("grippy"));
  this.tabpanel.appendChild(splitter);

  var sidebar = document.createElement("vbox");
  this.tabpanel.appendChild(sidebar);

  // toolbar
  var toolbox = document.createElement("toolbox");
  vbox.appendChild(toolbox);
  vbox.setAttribute("flex", 1);
  var toolbar = document.createElement("toolbar");
  toolbox.appendChild(toolbar);

  var toolbarbuttonStepInto = document.createElement("toolbarbutton");
  toolbarbuttonStepInto.setAttribute("label", "Step Into");
  toolbarbuttonStepInto.setAttribute("class", "debug-toolbar-button-stepinto");
  toolbarbuttonStepInto.setAttribute("tooltiptext", "Step to the next statement. If there is a function call involved it will break on the first statement in that function");
  toolbar.appendChild(toolbarbuttonStepInto);
  toolbarbuttonStepInto.onclick = bind("onstepinto", controller);

  var toolbarbuttonStepOver = document.createElement("toolbarbutton");
  toolbarbuttonStepOver.setAttribute("label", "Step Over");
  toolbarbuttonStepOver.setAttribute("tooltiptext", "Step over the next statement. If there is a function call on the line from which the step_over is issued then the despectator engine will stop at the statement after the function call in the same scope as from where the command was issued");
  toolbarbuttonStepOver.setAttribute("class", "debug-toolbar-button-stepover");
  toolbar.appendChild(toolbarbuttonStepOver);
  toolbarbuttonStepOver.onclick = bind("onstepover", controller);

  var toolbarbuttonStepOut = document.createElement("toolbarbutton");
  toolbarbuttonStepOut.setAttribute("label", "Step Out");
  toolbarbuttonStepOut.setAttribute("tooltiptext", "Steps out of the current scope and breaks on the statement after returning from the current function.");
  toolbarbuttonStepOut.setAttribute("class", "debug-toolbar-button-stepout");
  toolbar.appendChild(toolbarbuttonStepOut);
  toolbarbuttonStepOut.onclick = bind("onstepout", controller);

  toolbar.appendChild(document.createElement("toolbarspacer"));

  var toolbarbuttonRun = document.createElement("toolbarbutton");
  toolbarbuttonRun.setAttribute("label", "Run");
  toolbarbuttonRun.setAttribute("tooltiptext", "Starts or resumes the script until a new breakpoint is reached, or the end of the script is reached.");
  toolbarbuttonRun.setAttribute("class", "debug-toolbar-button-run");
  toolbar.appendChild(toolbarbuttonRun);
  toolbarbuttonRun.onclick = bind("onrun", controller);

  var toolbarbuttonStop = document.createElement("toolbarbutton");
  toolbarbuttonStop.setAttribute("label", "Stop");
  toolbarbuttonStop.setAttribute("tooltiptext", "Ends execution of the script immediately.");
  toolbarbuttonStop.setAttribute("class", "debug-toolbar-button-stop");
  toolbar.appendChild(toolbarbuttonStop);
  toolbarbuttonStop.onclick = bind("onstop", controller);

  var box = document.createElement("hbox");
  vbox.appendChild(box);
  box.setAttribute("flex", 1);
  box.setAttribute("style", "overflow-y:scroll");

  this.fileview = document.createElementNS("http://www.w3.org/1999/xhtml", "html:div");
  this.fileview.setAttribute("flex", 1);
  box.appendChild(this.fileview);

  // sidebar
  this.breakpoints_button = document.createElement("button");
  this.breakpoints_button.setAttribute("type", "menu");
  this.breakpoints_button.setAttribute("label", "Breakpoints");
  this.breakpoints_button.setAttribute("class", "breakpoint");
  this.breakpoints_button.setAttribute("align", "left");
  this.breakpoints_button.setAttribute("disabled", true);
  sidebar.appendChild(this.breakpoints_button);
  this.breakpoints_menu = document.createElement("menupopup");
  this.breakpoints_button.appendChild(this.breakpoints_menu);

  this.callstack = document.createElement("richlistbox");
  this.callstack.setAttribute("flex", 1);
  this.callstack.onselect = bind("onselectstack", controller);
  sidebar.appendChild(this.callstack);

  var vertsplitter = document.createElement("splitter");
  vertsplitter.setAttribute("collapse", "before");
  vertsplitter.appendChild(document.createElement("grippy"));
  sidebar.appendChild(vertsplitter);

  this.contexts = document.createElement("vbox");
  this.contexts.setAttribute("flex", 1);
  sidebar.appendChild(this.contexts);

  this.setSessionName("Debug Session");
  this.tab.click();

  } catch (err) {
    spectator.debug(err);
  }
};

spectator.SessionWindowView.prototype.updateBreakpoint = function() {
  try {
    var document = this.breakpoints_menu.ownerDocument;
    while (this.breakpoints_menu.firstChild) {
      this.breakpoints_menu.removeChild(this.breakpoints_menu.firstChild);
    }
    var points = this.breakpoints.getAll();
    var self = this;
    mapcar(
      function(point) {
        var menuitem = document.createElement("menuitem");
        menuitem.setAttribute("label", point.file + " : " + point.lineno);
        menuitem.setAttribute("value", point.id);
        self.breakpoints_menu.appendChild(menuitem);
      },
      points);
    this.breakpoints_button.setAttribute("disabled", points.length == 0);
  } catch (err) {
    spectator.debug(err);
  }
};

spectator.SessionWindowView.prototype.removeBreakpoint = function(id) {
  var document = this.breakpoints_menu.ownerDocument;
  mapcar(
    function(menuitem) {
      if (menuitem.value == id) {
        this.breakpoints_menu.removeChild(menuitem);
      }
    },
    this.breakpoints_menu.getElementsByTagName("menuitem"));
  if (this.breakpoints_menu.getElementsByTagName("menuitem").length == 0) {
    this.breakpoints_button.setAttribute("disabled", true);
  }
};

spectator.SessionWindowView.prototype.destroy = function() {
  this.tabpanels.removeChild(this.tabpanel);
  this.tabs.removeChild(this.tab);
  var tabs = this.tabs.getElementsByTagName("tab");
  if (tabs.length > 0) {
    tabs[tabs.length - 1].click();
  }
};

spectator.SessionWindowView.prototype.onfilenamechange = function(filename, lineno) {
  var self = this;
  var node = this.fileview;
  var document = node.ownerDocument;
  var makeBreakpointToggleHandler = function(span, num) {
    return function() {
      if (self.breakpoints.hasBreakpoint(filename, num)) {
        self.breakpoints.removeBreakpoint(filename, num);
        span.setAttribute("class", "lineno");
      } else {
        self.breakpoints.addBreakpoint(filename, num);
        span.setAttribute("class", "lineno_breakpoint");
      }
    };
  };
  this.remoteFiles.getFile(filename)
  .addCallback(
    function(data) {
      self.fileAvailable = true;
      while (node.firstChild) {
        node.removeChild(node.firstChild);
      }
      var points = {};
      mapcar(
        function(point) {
          points[points.lineno] = true;
        },
        self.breakpoints.getForFile(filename));
      var num = 1;
      mapcar(
        function(line) {
          var span = document.createElementNS("http://www.w3.org/1999/xhtml", "html:span");
          var span_lineno = document.createElementNS("http://www.w3.org/1999/xhtml", "html:b");
          if (points[num]) {
            span_lineno.setAttribute("class", "lineno_breakpoint");
          } else {
            span_lineno.setAttribute("class", "lineno");
          }
          span_lineno.appendChild(document.createTextNode(num));
          span_lineno.onclick = makeBreakpointToggleHandler(span_lineno, num);
          span.appendChild(span_lineno);
          // span.appendChild(document.createTextNode(line));
          span.appendChild(document.createTextNode(line.replace(/ /g, "\u00A0\u200B"))); // to ensure fixed-width + linewrapping
          node.appendChild(span);
          num++;
        },
        data.split(/\xx0D\x0A|\x0A|\x0D/));
      self.onlinenochange(lineno);
    })
  .addErrback(
    function() {
      self.fileAvailable = false;
      while (node.firstChild) {
        node.removeChild(node.firstChild);
      }
      var span = document.createElementNS("http://www.w3.org/1999/xhtml", "html:span");
      span.setAttribute("class", "unavailable");
      span.appendChild(document.createTextNode("Unable to open stream: " + filename));
      node.appendChild(span);
      self.onlinenochange(lineno);
    }
  );
};

spectator.SessionWindowView.prototype.onlinenochange = function(lineno) {
  if (this.fileAvailable) {
    var spans = this.fileview.getElementsByTagName("html:span");
    var l = lineno - 1;
    for (var i=0; i < spans.length; i++) {
      if (i == l) {
        spans[i].className = "current";
        maybeScrollIntoView(spans[i]);
      } else {
        spans[i].className = "";
      }
    }
  }
};

var isInView = function(element) {
  // todo: this doesn't work as it should ... go fix
  return false;
  var top = 0;
  var e = element;
  do {
    top += e.offsetTop;
  } while (e = e.offsetParent);
  var scrollTop = element.scrollTop;
  return !(top < scrollTop || top > scrollTop + window.innerHeight - element.offsetHeight);
}

var maybeScrollIntoView = function(element) {
  if (!isInView(element)) {
    element.scrollIntoView();
  }
};

spectator.SessionWindowView.prototype.onstackupdate = function(callstack) {
  var node = this.callstack;
  var document = node.ownerDocument;
  while (node.firstChild) {
    node.removeChild(node.firstChild);
  }
  mapcar(
    function(stack) {
      var listitem = document.createElement("richlistitem");
      listitem.setAttribute("value", stack.level);
      var label = document.createElement("label");
      label.appendChild(document.createTextNode("#" + stack.level + " " + stack.where));
      var file = document.createElement("description");
      file.appendChild(document.createTextNode("in: " + unescape(stack.filename)));
      var line = document.createElement("description");
      line.appendChild(document.createTextNode("on line: " + stack.lineno));
      var vbox = document.createElement("vbox");
      vbox.appendChild(label);
      vbox.appendChild(file);
      vbox.appendChild(line);
      listitem.appendChild(vbox);
      node.appendChild(listitem);
      if (stack.level == 0) {
        node.selectedItem = listitem;
      }
    },
    callstack);
};

spectator.SessionWindowView.prototype.oncontextupdate = function(contexts) {
  var self = this;
  var node = this.contexts;
  var document = node.ownerDocument;
  while (node.firstChild) {
    node.removeChild(node.firstChild);
  }
  var loadTreeChildren = function(properties) {
    var treechildren = document.createElement("treechildren");
    mapcar(
      function(property) {
        var treeitem = document.createElement("treeitem");
        treeitem.setAttribute("label", property.name + " : " + (property.classname || property.type));
        if (property.property) {
          treeitem.setAttribute("container", true);
          treeitem.appendChild(loadTreeChildren(property.property));
        } else if (property._value_) {
          treeitem.setAttribute("container", true);
          treeitem.setAttribute("open", true);
          var valuecontainer = document.createElement("treechildren");
          var valueitem = document.createElement("treeitem");
          valueitem.setAttribute("label", property._value_);
          valuecontainer.appendChild(valueitem);
          treeitem.appendChild(valuecontainer);
        }
        treechildren.appendChild(treeitem);
      },
      properties);
    return treechildren;
  };
  var allTrees = [];
  mapcar(
    function(context) {
      var tree = document.createElement("tree");
      tree.setAttribute("flex", 1);
      tree.setAttribute("hidecolumnpicker", true);
      tree.setAttribute("seltype", "single");
      var treecols = document.createElement("treecols");
      tree.appendChild(treecols);
      var treecol = document.createElement("treecol");
      treecol.setAttribute("flex", 1);
      treecol.setAttribute("label", context.name);
      treecol.setAttribute("primary", true);
      treecols.appendChild(treecol);
      if (context.property) {
        tree.appendChild(loadTreeChildren(context.property));
      }
      node.appendChild(tree);
      tree.contextId = context.context;
      allTrees.push(tree);
      tree.onfocus = function() {
        self.currentContextId = tree.contextId;
        mapcar(
          function(t) {
            if (t == tree) {
              t.setAttribute("flex", 1);
            } else {
              t.setAttribute("flex", 0);
            }
          },
          allTrees);
      };
    },
    contexts);
  var found = false;
  mapcar(
    function(tree) {
      if (tree.contextId == self.currentContextId) {
        tree.focus();
        found = true;
      }
    },
    allTrees);
  if (!found) {
    allTrees[0].onfocus();
  }
};

